package com.rs.game;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;

import com.rs.Settings;
import com.rs.game.Hit.HitLook;
import com.rs.game.player.Player;
import com.rs.game.player.Skills;
import com.rs.utils.Utils;


public abstract class Entity extends WorldTile {
	
	private static final long serialVersionUID = -3372926325008880753L;
	
	//transient stuff
	private transient int index;
	private transient int lastRegionId; //the last region the entity was at
	private transient WorldTile lastLoadedMapRegionTile;
	private transient CopyOnWriteArrayList<Integer> mapRegionsIds; //called by more than 1thread so concurent
	private transient int direction;
	private transient WorldTile nextWorldTile;
	private transient int nextWalkDirection;
	private transient int nextRunDirection;
	private transient WorldTile nextFaceWorldTile;
	private transient boolean removeEntityOnTele;
	private transient boolean teleported;
	private transient ConcurrentLinkedQueue<int[]> walkSteps;//called by more than 1thread so concurent
	private transient ConcurrentLinkedQueue<Hit> receivedHits;
	private transient boolean finished; //if removed
	private transient long freezeDelay;
	//entity masks
	private transient Animation nextAnimation; 
	private transient Graphics nextGraphics;
	private transient Hit nextHit1;
	private transient Hit nextHit2;
	private transient int nextFaceEntity;
	private transient int lastFaceEntity;
	private transient Entity attackedBy; //whos attacking you, used for single
	private transient long attackedByDelay; //delay till someone else can attack you
	private transient boolean multiArea;
	private transient boolean isAtDynamicRegion;
	//saving stuff
	private int hitpoints;
	private int mapSize; //default 0, can be setted other value usefull on static maps
	private boolean run;
	//creates Entity and saved classes
	public Entity(WorldTile tile) {
		super(tile);
	}

	public boolean moved() {
		ConcurrentLinkedQueue<int[]> temp = walkSteps;
		while(true) {
			if(walkSteps != temp) {
				return true;
			}
		}
	}
	
	public final void initEntity() {
		mapRegionsIds = new CopyOnWriteArrayList<Integer>();
		walkSteps = new ConcurrentLinkedQueue<int[]>();
		receivedHits= new ConcurrentLinkedQueue<Hit>();
		nextWalkDirection = -1;
		lastFaceEntity = -1;
		nextFaceEntity = -2;
	}
	
	public int getClientIndex() {
		return index + (this instanceof Player ? 32768 : 0);
	}
	
	public void applyHit(Hit hit) {
		if(isDead())
			return;
		//todo damage for who gets drop
		receivedHits.add(hit); //added hit first because, soaking added after, if applyhit used right there shouldnt be any problem
		handleIngoingHit(hit);
	}
	
	public abstract void handleIngoingHit(Hit hit);
	
	public void reset() {
		setHitpoints(getMaxHitpoints());
		receivedHits.clear();
		walkSteps.clear();
		freezeDelay = 0;
		attackedBy = null;
		attackedByDelay = 0;
	}
	
	public void processReceivedHits() {
		if(nextHit1 == null) {
			Hit hit = receivedHits.poll();
			if(hit == null) //if theres no hit why would be for nexthit2
				return;
			processHit(hit);
		}
		if(nextHit2 == null) {
			Hit hit = receivedHits.poll();
			if(hit == null)
				return;
			processHit(hit);
		}
	}

	private void processHit(Hit hit) {
		if(isDead())
			return;
		if(nextHit1 == null)
			nextHit1 = hit;
		else if (nextHit2 == null)
			nextHit2 = hit;
		else {
			receivedHits.add(hit);
			return;
		}
		removeHitpoints(hit);
	}
	
	public void removeHitpoints(Hit hit) {
		if(isDead() || hit.getLook() == HitLook.Absorve_Hit)
			return;
		if(hit.getDamage() > hitpoints)
			hit.setDamage(hitpoints);
		hitpoints -= hit.getDamage();
		if(hitpoints <= 0)
			sendDeath(hit.getSource());
		else if (this instanceof Player){
			Player player = (Player) this;
			if(player.getPrayer().hasPrayersOn()) {
				if((hitpoints < player.getMaxHitpoints()*0.1) && player.getPrayer().usingPrayer(0, 23)) {
					nextGraphics = new Graphics(436);
					hitpoints += player.getSkills().getLevelForXp(Skills.PRAYER) * 2.5;
					player.getSkills().set(Skills.PRAYER, 0);
				}
			}
		}
	}
	
	public void heal(int ammount) {
		hitpoints = hitpoints+ammount >= getMaxHitpoints() ? getMaxHitpoints() : hitpoints+ammount;
	}
	
	public boolean hasWalkSteps() {
		return !walkSteps.isEmpty();
	}
	
	public abstract void sendDeath(Entity source);
	
	public void processMovement() {
		if(lastFaceEntity >= 0) {
			Entity target = lastFaceEntity >= 32768 ? World.getPlayers().get(lastFaceEntity-32768) : World.getNPCs().get(lastFaceEntity);
			if(target != null)
				direction = Utils.getFaceDirection(getX()-target.getCoordFaceX(target.getSize()), getY()-target.getCoordFaceY(target.getSize()));
		}
		nextWalkDirection = nextRunDirection = -1;
		if(nextWorldTile != null) {
			setLocation(nextWorldTile);
			nextWorldTile = null;
			teleported = true;
			World.updateEntityRegion(this);
			if(needMapUpdate())
				loadMapRegions();
			return;
		}
		teleported = false;
		if(walkSteps.isEmpty())
			return;
		nextWalkDirection = getNextWalkStep();
		if(nextWalkDirection != -1) {
			moveLocation(Utils.DIRECTION_DELTA_X[nextWalkDirection], Utils.DIRECTION_DELTA_Y[nextWalkDirection], 0);
			if(run) {
				if(this instanceof Player && ((Player) this).getRunEnergy() <= 0)
					setRun(false);
				else {
					nextRunDirection = getNextWalkStep();
					if(nextRunDirection != -1) {
						if(this instanceof Player)
							((Player) this).drainRunEnergy();
						moveLocation(Utils.DIRECTION_DELTA_X[nextRunDirection], Utils.DIRECTION_DELTA_Y[nextRunDirection], 0);
					}
				}
			}
		}
		World.updateEntityRegion(this);
		if(needMapUpdate())
			loadMapRegions();
	}
	
	@Override
	public void moveLocation(int xOffset, int yOffset, int planeOffset) {
		direction = Utils.getFaceDirection(xOffset, yOffset);
		super.moveLocation(xOffset, yOffset, planeOffset);
	}
	
	private boolean needMapUpdate() {
		int lastMapRegionX = lastLoadedMapRegionTile.getRegionX();
		int lastMapRegionY = lastLoadedMapRegionTile.getRegionY();
		int regionX = getRegionX();
		int regionY = getRegionY();
		int size = (Settings.MAP_SIZES[mapSize] >> 4) - 2;
		return ((lastMapRegionX-regionX) >= size || (lastMapRegionX-regionX) <= -size) || ((lastMapRegionY-regionY) >= size || (lastMapRegionY-regionY) <= -size);
	}
	
	public boolean addWalkSteps(int destX, int destY) {
		return addWalkSteps(destX, destY, -1);
	}
	
	/*
	 * return added all steps
	 */
	public boolean addWalkStepsInteract(final int destX, final int destY, int maxStepsCount, int size, boolean calculate) {
		int[] lastTile = getLastWalkTile();
		int myX = lastTile[0];
		int myY = lastTile[1];
		int stepCount = 0;
		while(true) {
			stepCount++;
			int myRealX = myX;
			int myRealY = myY;
			
			if (myX < destX)
				 myX++;
			else if (myX > destX)
				myX--;
			if (myY < destY)
				myY++;
			else if (myY > destY)
				myY--;
				if(!addWalkStep(myX, myY, lastTile[0], lastTile[1], true)) {
					if(!calculate)
						return false;
					myX = myRealX;
					myY = myRealY;
					int[] myT = calculatedStep(myRealX, myRealY, destX, destY, lastTile[0], lastTile[1], size);
					if(myT == null)
						return false;
					myX = myT[0];
					myY = myT[1];
				}
				int distanceX = myX - destX;
				int distanceY = myY - destY;
				if(!(distanceX > size || distanceX < -1 || distanceY > size || distanceY < -1))
					return true;
				if(stepCount == maxStepsCount)
					return true;
				lastTile[0] = myX;
				lastTile[1] = myY;
				if(lastTile[0] == destX && lastTile[1] == destY)
					return true;
		}
	}
	
	public int[] calculatedStep(int myX, int myY, int destX, int destY, int lastX, int lastY, int size) {
		if (myX < destX) {
			 myX++;
			 if(!addWalkStep(myX, myY, lastX, lastY, true))
				 myX--;
			 else if(!(myX - destX > size || myX - destX < -1 || myY - destY > size || myY - destY < -1)) {
					if(myX == lastX || myY == lastY)
						return null;
					return new int[] {myX, myY};
				}
		}else if (myX > destX) {
			 myX--;
			 if(!addWalkStep(myX, myY, lastX, lastY, true))
				 myX++;
			 else if(!(myX - destX > size || myX - destX < -1 || myY - destY > size || myY - destY < -1)) {
					if(myX == lastX || myY == lastY)
						return null;
					return new int[] {myX, myY};
				}
		}
		if (myY < destY) {
			myY++;
			 if(!addWalkStep(myX, myY, lastX, lastY, true))
				 myY--;
			 else if(!(myX - destX > size || myX - destX < -1 || myY - destY > size || myY - destY < -1)) {
					if(myX == lastX || myY == lastY)
						return null;
					return new int[] {myX, myY};
			}
		}else if (myY > destY) {
			myY--;
			if(!addWalkStep(myX, myY, lastX, lastY, true))
				myY++;
			 else if(!(myX - destX > size || myX - destX < -1 || myY - destY > size || myY - destY < -1)) {
					if(myX == lastX || myY == lastY)
						return null;
					return new int[] {myX, myY};
			 }
		}
		if(myX == lastX || myY == lastY)
			return null;
		return new int[] {myX, myY};
	}
	
	
	/*
	 * return added all steps
	 */
	public boolean addWalkSteps(final int destX, final int destY, int maxStepsCount) {
		return addWalkSteps(destX, destY, -1, true);
	}
	
	
	/*
	 * return added all steps
	 */
	public boolean addWalkSteps(final int destX, final int destY, int maxStepsCount, boolean check) {
		int[] lastTile = getLastWalkTile();
		int myX = lastTile[0];
		int myY = lastTile[1];
		int stepCount = 0;
		while(true) {
			stepCount++;
			if (myX < destX)
				 myX++;
			else if (myX > destX)
				myX--;
			if (myY < destY)
				myY++;
			else if (myY > destY)
				myY--;
			if(!addWalkStep(myX, myY, lastTile[0], lastTile[1], check)) //cliped here so stop
				return false;
			if(stepCount == maxStepsCount)
				return true;
			lastTile[0] = myX;
			lastTile[1] = myY;
			if(lastTile[0] == destX && lastTile[1] == destY)
				return true;
		}
	}
	
	public int[] getLastWalkTile() {
		Object[] objects = walkSteps.toArray();
		if(objects.length == 0)
			return new int[] {getX(), getY()};
		int step[] = (int[]) objects[objects.length-1];
		return new int[] {step[1], step[2]};
	}
	
	//return cliped step
	public boolean addWalkStep(int nextX, int nextY, int lastX, int lastY, boolean check) {
		int dir = Utils.getFaceDirection(nextX-lastX, nextY-lastY);
		if (dir == -1) 
			return false;
		
		if(check && !World.checkWalkStep(getPlane(), lastX, lastY, dir, getSize())) {
			return false;
		}
		walkSteps.add(new int[] {dir, nextX , nextY});
		return true;
	}

	public void resetWalkSteps() {
		walkSteps.clear();
	}
	
	private int getNextWalkStep() {
		int step[] = walkSteps.poll();
		if(step == null)
			return -1;
		return step[0];
		
	}
	
	public boolean restoreHitPoints() {
		if(hitpoints > getMaxHitpoints()) {
			if(this instanceof Player) {
				Player player = (Player) this;
				if(player.getPrayer().usingPrayer(1, 5) && Utils.getRandom(100) <= 15)
					return false;
			}
			hitpoints -= 1;
			return true;
		} else if (hitpoints < getMaxHitpoints()) {
			hitpoints += 1;
			return true;
		}
		return false;
	}
	public boolean needMasksUpdate() {
		return nextFaceEntity != -2 || nextAnimation != null || nextGraphics != null || nextFaceWorldTile != null || nextHit1 != null || nextHit2 != null;
	}

	public boolean isDead() {
		return hitpoints == 0;
	}
	public void resetMasks() {
		nextAnimation = null;
		nextGraphics = null;
		nextFaceWorldTile = null;
		nextHit1 = null;
		nextHit2 = null;
		nextFaceEntity = -2;
	}
	
	public abstract void finish();
	public abstract int getMaxHitpoints();
	public abstract void processEntity();
	
	public void loadMapRegions() {
		mapRegionsIds.clear();
		isAtDynamicRegion = false;
		int regionX = getRegionX();
		int regionY = getRegionY();
		int mapHash = Settings.MAP_SIZES[mapSize] >> 4;
		for (int xCalc = (regionX - mapHash) / 8; xCalc <= ((regionX + mapHash) / 8); xCalc++)
			for (int yCalc = (regionY - mapHash) / 8; yCalc <= ((regionY + mapHash) / 8); yCalc++) {
				int regionId = yCalc + (xCalc << 8);
				if(World.getRegion(regionId) instanceof DynamicRegion)
					isAtDynamicRegion = true;
		    		mapRegionsIds.add(yCalc + (xCalc << 8));
			}
		lastLoadedMapRegionTile = new WorldTile(this); //creates a immutable copy of this
	}
	
	public void setIndex(int index) {
		this.index = index;
	}

	public int getIndex() {
		return index;
	}

	public int getHitpoints() {
		return hitpoints;
	}

	public void setHitpoints(int hitpoints) {
		this.hitpoints = hitpoints;
		
	}
	
	public void setLastRegionId(int lastRegionId) {
		this.lastRegionId = lastRegionId;
	}

	public int getLastRegionId() {
		return lastRegionId;
	}
	
	public int getMapSize() {
		return mapSize;
	}
	
	public CopyOnWriteArrayList<Integer> getMapRegionsIds() {
		return mapRegionsIds;
	}

	public void setNextAnimation(Animation nextAnimation) {
		this.nextAnimation = nextAnimation;
	}

	public Animation getNextAnimation() {
		return nextAnimation;
	}

	public void setNextGraphics(Graphics nextGraphics) {
		this.nextGraphics = nextGraphics;
	}

	public Graphics getNextGraphics() {
		return nextGraphics;
	}

	public void setNextHit1(Hit nextHit1) {
		this.nextHit1 = nextHit1;
	}

	public Hit getNextHit1() {
		return nextHit1;
	}

	public void setNextHit2(Hit nextHit2) {
		this.nextHit2 = nextHit2;
	}

	public Hit getNextHit2() {
		return nextHit2;
	}

	public void setDirection(int direction) {
		this.direction = direction;
	}

	public int getDirection() {
		return direction;
	}

	public void setFinished(boolean finished) {
		this.finished = finished;
	}

	public boolean hasFinished() {
		return finished;
	}
	
	public void setNextWorldTile(WorldTile nextWorldTile) {
		setNextWorldTile(nextWorldTile, true);
	}

	public void setNextWorldTile(WorldTile nextWorldTile, boolean removeEntity) {
		this.nextWorldTile = nextWorldTile;
		removeEntityOnTele = removeEntity;
	}

	public boolean getRemoveEntityOnTele() {
		return removeEntityOnTele;
	}
	
	public WorldTile getNextWorldTile() {
		return nextWorldTile;
	}
	
	public boolean hasTeleported() {
		return teleported;
	}
	
	public WorldTile getLastLoadedMapRegionTile() {
		return lastLoadedMapRegionTile;
	}
	
	public int getNextWalkDirection() {
		return nextWalkDirection;
	}
	
	public int getNextRunDirection() {
		return nextRunDirection;
	}
	
	public void setRun(boolean run) {
		this.run = run;
	}
	
	public boolean getRun() {
		return run;
	}

	public WorldTile getNextFaceWorldTile() {
		return nextFaceWorldTile;
	}

	public void setNextFaceWorldTile(WorldTile nextFaceWorldTile) {
		this.nextFaceWorldTile = nextFaceWorldTile;
		direction = Utils.getFaceDirection(getX()-nextFaceWorldTile.getX(), getY()-nextFaceWorldTile.getY());
	}
	
	public abstract int getSize();


	public void setNextFaceEntity(Entity entity) {
		if(entity == null) {
			nextFaceEntity = -1;
			lastFaceEntity = -1;
		} else{
			nextFaceEntity = entity.getClientIndex();
			lastFaceEntity = nextFaceEntity;
		}
	}
	
	public int getNextFaceEntity() {
		return nextFaceEntity;
	}

	public long getFreezeDelay() {
		return freezeDelay;
	}

	public int getLastFaceEntity() {
		return lastFaceEntity;
	}
	
	public void addFreezeDelay(long time) {
		this.freezeDelay = time+System.currentTimeMillis();
		if(System.currentTimeMillis() > freezeDelay)  {
			if(this instanceof Player) {
				Player p = (Player) this;
				p.getPackets().sendGameMessage("You have been froozen.");
			}
		}
		resetWalkSteps();
	}
	
	public abstract double getMagePrayerMultiplier();
	public abstract double getRangePrayerMultiplier();
	public abstract double getMeleePrayerMultiplier();

	public Entity getAttackedBy() {
		return attackedBy;
	}

	public void setAttackedBy(Entity attackedBy) {
		this.attackedBy = attackedBy;
	}

	public long getAttackedByDelay() {
		return attackedByDelay;
	}

	public void setAttackedByDelay(long attackedByDelay) {
		this.attackedByDelay = attackedByDelay;
	}
	
	public void checkMultiArea() {
		multiArea = World.isMultiArea(this);
	}
	
	public boolean isAtMultiArea() {
		return multiArea;
	}
	
	public void setAtMultiArea(boolean multiArea) {
		this.multiArea = multiArea;
	}

	public boolean isAtDynamicRegion() {
		return isAtDynamicRegion;
	}

}
